package com.ease.framework {

import com.ease.util.MixedUtil;
import com.ease.util.ObjectEvent;
import com.ease.util.ObjectFilter;
import com.ease.util.ReflectionUtil;
import com.ease.util.RelationshipUpdateEvent;
import com.ease.util.Set;
import com.ease.util.collections.TypeBasedObjectCache;
import com.ease.util.gc.SharedReference;

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;

import mx.core.FlexGlobals;


[Event(name="objectAdded", type="com.ease.util.ObjectEvent")]
[Event(name="objectRemoved", type="com.ease.util.ObjectEvent")]
[Event(name="roleAssigned", type="com.ease.util.RelationshipUpdateEvent")]
/**
 * This class maintains a registry of all objects that it discovers through the events of
 * type ObjectEvent. The events come from the associated event bus, and in case of the
 * global instance of this class, they may also come from the global Application instance.
 *
 * <p>An <code>ObjectRegistry</code> can have child registries associated with it. The
 * parent registry tracks (or contains) only those objects that are not tracked by one of
 * the child registries. All <code>ObjectEvent</code>s raised on the child registries are
 * relayed or redispatched by this registry.
 *
 * <p>An object add/remove or a relationship update may be notified more than once on this
 * registry if the sources of events for this registry send such multiple notifications.
 * It's the responsibility of the clients of this registry to handle such duplicate events
 * (e.g., through idempotency).
 *
 * <p><b>Generic Injection:</b>
 *
 * <p>ObjectRegistry serves as a consumer for InjectionManager, i.e. provides for
 * injection of other objects into the objects maintained by this registry, through the
 * general purpose injection tag [Injectable].
 *
 * <p>The registry also serves as a provider for InjectionManager, but supplies only those
 * objects that are allowed to be strong referenced, since the owners of injectable
 * properties may create strong references to the injected objects. Furthermore, the
 * objects in the child registries are not served by this directory for injection
 * purposes; only those objects that are found in this registry itself are included.
 *
 * <p><b>Event Based Object Registration:</b>
 *
 * <p>ObjectRegistry also mediates the generic "objectAdded" and "objectRemoved" events,
 * which the application can dispatch on any view (with bubbles equal to true), or on the
 * global Application instance, or any other EventDispatcher that is already on a frinje
 * EventBus. These events signify addition/removal of any arbitrary object to the
 * application, such that the object wishes to take advantage of the services like event
 * relaying on the event bus and dependency injection, which are normally afforded to
 * views and mediators only, and only when they're added to the display.
 *
 * <p>Such an event can be of any class, but is expected to have a property by the
 * name "object", that points to the object of interest. The reason frinje doesn't require
 * a typed event (such as <code>ObjectEvent</code>) is that it doesn't wish to impose
 * <i>any</i> platform libraries on the application, not even flex-util. Considering that
 * object addition/removal events enable the use of various frinje services, even an
 * application that doesn't include flex-util can take advantage of most frinje services.
 *
 * <p><b>Context Propagation:</b>
 *
 * <p>Additionally, ObjectRegistry serves as a client of ContextPropagationManager in that
 * the objects (both views and mediators) added to the registry can use the [Inheritable]
 * tag to mark properties for propagation down a composition hierarchy.
 *
 * @see InjectionManager
 * @see ContextPropagationManager
 * @see EventBus
 * @see ObjectEvent
 */
public class ObjectRegistry extends EventDispatcher
                            implements IObjectModelSource, IObjectDirectory
{
   private static var _instance:ObjectRegistry;
   private var _eventBusRef:SharedReference = SharedReference.newReference(EventBus);
   private var _injectionMgrRef:SharedReference =
         SharedReference.newReference(InjectionManager);
   private var _contextPropagationMgrRef:SharedReference =
         SharedReference.newReference(ContextPropagationManager);
   private var _objects:Set = new Set( true );
   private var _objectsByTypeCache:TypeBasedObjectCache;
   private var _childRegistries:Array/*IObjectContainer*/ = [];

   /**
    * A global instance of this class. This is the instance that would typically be used
    * by an application.
    *
    * @return
    *    An instance of ObjectRegistry for global use.
    */
   public static function get instance():ObjectRegistry {
      if (_instance == null) {
         _instance = new ObjectRegistry( EventBus.instance,
                                         InjectionManager.instance,
                                         ContextPropagationManager.instance );
         _instance.globalInstanceInit();
      }
      return _instance;
   }

   /**
    * Initializes an instance of this class.
    *
    * @param eventBus
    *    The EventBus associated with this registry.
    *
    * @param injectionMgr
    *    The InjectionManager associated with this registry.
    *
    * @param propertyPropagationMgr
    *    The ContextPropagationManager associated with this registry.
    *
    * @see #instance
    * @see #eventBus
    */
   public function
   ObjectRegistry( eventBus:EventBus = null, injectionMgr:InjectionManager = null,
                   contextPropagationMgr:ContextPropagationManager = null ) {
      _objectsByTypeCache = new TypeBasedObjectCache();
      _objectsByTypeCache.usesConcreteTypeOnly = false;
      if (eventBus == null) {
         eventBus = new EventBus();
      }
      setEventBus(eventBus);
      this.injectionMgr = injectionMgr;
      if (injectionMgr != null) {
         injectionMgr.addConsumer( this );
         injectionMgr.addProvider( this );
      }
      this.contextPropagationMgr = contextPropagationMgr;
      if (contextPropagationMgr != null) {
         contextPropagationMgr.addTargetRegistry( this );
      }
      // Listen for object add/remove and relationship update notifications.
      addEventListenersTo( this.eventBus );
   }

   //------------------------------------------------------------------------------------
   // Instance properties
   // The property provider used avoids directly referencing the global instance.
   // This is done to simplify the object graph for Memory Profiler.

   //-------------------------------------------------------
   // Property: eventBus

   /**
    * The EventBus associated with this registry. The registry is responsible for
    * processing the <code>ObjectEvent</code>s raised on this bus.
    */
   public function get eventBus():EventBus {
      return _eventBusRef.object;
   }

   private function setEventBus(value:EventBus):void {
      _eventBusRef.object = value
   }

   //---------------------------------------------------------------------------
   // Instance Methods

   /**
    * @inheritDoc
    */
   public final function contains( item:Object ):Boolean {
      if (_objects.contains( item )) {
         return true;
      }
      if (childRegistriesContain( item )) {
         return true;
      }
      return false;
   }

   /**
    * Adds a child registry to this registry.
    *
    * @param registry
    *    The registry to be added as a child.
    */
   public final function addChildRegistry( registry:IObjectModelSource ):void {
      _childRegistries.push( registry );
      addEventListenersTo( registry );
   }

   /**
    * Removes a child registry from this registry.
    *
    * @param registry
    *    The child registry to be removed.
    */
   public final function removeChildRegistry( registry:IObjectModelSource ):void {
      MixedUtil.removeItemFromArray( registry, _childRegistries );
      removeEventListenersFrom( registry );
   }

   /**
    * Matches the objects in this registry against the given spec. Child registries are
    * not included in this lookup.
    *
    * @see IObjectDirectory#getObjects()
    */
   public final function getObjects( spec:ObjectFilter, callback:Function,
                                     moduleReference:* = null,
                                     currentObjectSet:Array/*?*/ = null ):void {
      if (spec.metaPropertyFilterCount > 0) {
         // We don't filter on any meta-properties, which means that if there is some
         // meta-property filter defined, we can't ensure that our objects satisfy this
         // filter. Since the object provider (i.e., this directory) is supposed
         // to ascertain meta-property filter, we can't return anything from here.
         callback( [] );
         return;
      }
      var objects:Array = _objectsByTypeCache.getObjects( spec.type );
      objects = spec.filter( objects );

      // Note: It'd be nice to find the intersection between currentObjectSet and objects,
      // and place the common items at the front of the array returned from here (in the
      // same order as they are in currentObjectSet), but doing so hasn't been found
      // necessary so far.

      callback( objects );
   }

   /**
    * @inheritDoc
    */
   public final function evaluateMetaFilter( spec:ObjectFilter, object:* ):Boolean {
      // This class doesn't maintain any meta-properties on its objects.
      return true;
   }

   private function globalInstanceInit():void {
      var app:EventDispatcher = FlexGlobals.topLevelApplication as EventDispatcher;
      // The global instance of this class listens for "ObjectEvent"s and
      // "RelationshipUpdateEvents"s on the global Application instance as well, i.e.,
      // not just on the associated event bus (so that even those objects can raise them
      // which are not already on some event bus).
      // Note that we don't (need to) use weak-referencing here (i.e., when adding the
      // listeners) because this global instance is for the lifetime of the app.
      // Also note that multiple notifications (i.e., from the bus as well as from the
      // Application, if that ever happens) are fine since object add/remove in this
      // class takes care of such multiplicity.
      addEventListenersTo( app );
   }

   private function addEventListenersTo( ed:IEventDispatcher ):void {
      ed.addEventListener( ObjectEvent.OBJECT_ADDED, onObjectAdded );
      ed.addEventListener( ObjectEvent.OBJECT_REMOVED, onObjectRemoved );
      ed.addEventListener( RelationshipUpdateEvent.ROLE_ASSIGNED, onRoleAssignment );
   }

   private function removeEventListenersFrom( ed:IEventDispatcher ):void {
      ed.removeEventListener( ObjectEvent.OBJECT_ADDED, onObjectAdded );
      ed.removeEventListener( ObjectEvent.OBJECT_REMOVED, onObjectRemoved );
      ed.removeEventListener( RelationshipUpdateEvent.ROLE_ASSIGNED, onRoleAssignment );
   }

   private function childRegistriesContain( object:Object ):Boolean {
      for each (var registry:IObjectModelSource in _childRegistries) {
         if (registry.contains( object )) {
            return true;
         }
      }
      return false;
   }

   private function onRoleAssignment( ev:RelationshipUpdateEvent ):void {
      var newEvent:Event = ev.clone();
      dispatchEvent( newEvent );
   }

   /**
    * Handles addition of an object to the application.
    */
   private function onObjectAdded( ev:Event ):void {
      var object:* = ReflectionUtil.getPropertyValue( ev, "object" );
      if (object == null) {
         return;
      }
      var foundInThisRegistry:Boolean = _objects.contains( object );

      // This registry keeps track of only those objects that are not tracked by the
      // child registries.
      var foundInChildRegistries:Boolean = childRegistriesContain( object );
      if (foundInChildRegistries) {
         _objects.remove(object);
         _objectsByTypeCache.remove( object );
      }

      if (foundInThisRegistry) {
         return;
      }

      var useWeakReference:Boolean = false;
      if (ev is ObjectEvent) {
         useWeakReference = ObjectEvent( ev ).isWeakReferencingMandatory;
      }

      if (! foundInChildRegistries) {
         _objects.add( object );
         if (! useWeakReference) {
            // That is, only if strong referencing is allowed!
            _objectsByTypeCache.add( object );
         }
      }
      // We add all objects we find out about to the associated event bus.
      this.eventBus.addParticipant( object, useWeakReference );

      // Redispatch the event for the clients of this registry.
      var newEvent:ObjectEvent;
      if (ev is ObjectEvent) {
         newEvent = ObjectEvent( ev.clone() );
      } else {
         newEvent = ObjectEvent.newAddition( object );
      }
      dispatchEvent( newEvent );
   }

   /**
    * Handles removal of an object from the application.
    */
   private function onObjectRemoved( ev:Event ):void {
      var object:* = ReflectionUtil.getPropertyValue( ev, "object" );
      if (object == null) {
         return;
      }
      if (_objects.contains( object )) {
         _objects.remove(object);
         _objectsByTypeCache.remove( object );
      }
      this.eventBus.removeParticipant( object );

      // Redispatch the event for the clients of this registry.
      var newEvent:ObjectEvent;
      if (ev is ObjectEvent) {
         newEvent = ObjectEvent( ev.clone() );
      } else {
         newEvent = ObjectEvent.newRemoval( object );
      }
      dispatchEvent( newEvent );
   }

   //------------------------------------------------------------------------------------
   // Instance properties
   // The property provider used avoids directly referencing the global instance.
   // This is done to simplify the object graph for Memory Profiler.

   private function get injectionMgr():InjectionManager {
      return _injectionMgrRef.object;
   }

   private function set injectionMgr(value:InjectionManager):void {
      _injectionMgrRef.object = value;
   }

   private function get contextPropagationMgr():ContextPropagationManager {
      return _contextPropagationMgrRef.object;
   }

   private function set contextPropagationMgr(value:ContextPropagationManager):void {
      _contextPropagationMgrRef.object = value;
   }
} // class

}